import React from "react"; import { ActivityIndicator, Alert, Pressable, View } from "react-native"; import { KeyboardAwareScrollView, KeyboardGestureArea, } from "react-native-keyboard-controller"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { router, Stack, useLocalSearchParams } from "expo-router"; import BookmarkTextMarkdown from "@/components/bookmarks/BookmarkTextMarkdown"; import TagPill from "@/components/bookmarks/TagPill"; import FullPageError from "@/components/FullPageError"; import { Button } from "@/components/ui/Button"; import ChevronRight from "@/components/ui/ChevronRight"; import { Divider } from "@/components/ui/Divider"; import FullPageSpinner from "@/components/ui/FullPageSpinner"; import { Input } from "@/components/ui/Input"; import { Skeleton } from "@/components/ui/Skeleton"; import { Text } from "@/components/ui/Text"; import { useToast } from "@/components/ui/Toast"; import { cn } from "@/lib/utils"; import { ChevronUp, RefreshCw, Sparkles, Trash2 } from "lucide-react-native"; import { useAutoRefreshingBookmarkQuery, useDeleteBookmark, useSummarizeBookmark, useUpdateBookmark, } from "@karakeep/shared-react/hooks/bookmarks"; import { useWhoAmI } from "@karakeep/shared-react/hooks/users"; import { BookmarkTypes, ZBookmark } from "@karakeep/shared/types/bookmarks"; import { isBookmarkStillTagging } from "@karakeep/shared/utils/bookmarkUtils"; function InfoSection({ className, ...props }: React.ComponentProps) { return ( ); } function TagList({ bookmark, readOnly, }: { bookmark: ZBookmark; readOnly: boolean; }) { return ( {isBookmarkStillTagging(bookmark) ? ( ) : ( bookmark.tags.length > 0 && ( <> {bookmark.tags.map((t) => ( ))} ) )} {!readOnly && ( router.push(`/dashboard/bookmarks/${bookmark.id}/manage_tags`) } className="flex w-full flex-row justify-between gap-3" > Manage Tags )} ); } function ManageLists({ bookmark }: { bookmark: ZBookmark }) { return ( router.push(`/dashboard/bookmarks/${bookmark.id}/manage_lists`) } className="flex w-full flex-row justify-between gap-3 rounded-lg" > Manage Lists ); } function TitleEditor({ title, setTitle, isPending, disabled, }: { title: string | null | undefined; setTitle: (title: string | null) => void; isPending: boolean; disabled?: boolean; }) { return ( setTitle(text)} defaultValue={title ?? ""} /> ); } function NotesEditor({ notes, setNotes, isPending, disabled, }: { notes: string | null | undefined; setNotes: (title: string | null) => void; isPending: boolean; disabled?: boolean; }) { return ( setNotes(text)} textAlignVertical="top" defaultValue={notes ?? ""} /> ); } function AISummarySection({ bookmark, readOnly, }: { bookmark: ZBookmark; readOnly: boolean; }) { const { toast } = useToast(); const [isExpanded, setIsExpanded] = React.useState(false); const { mutate: summarize, isPending: isSummarizing } = useSummarizeBookmark({ onSuccess: () => { toast({ message: "Summary generated successfully!", showProgress: false, }); }, onError: () => { toast({ message: "Failed to generate summary", showProgress: false, }); }, }); const { mutate: resummarize, isPending: isResummarizing } = useSummarizeBookmark({ onSuccess: () => { toast({ message: "Summary regenerated successfully!", showProgress: false, }); }, onError: () => { toast({ message: "Failed to regenerate summary", showProgress: false, }); }, }); const { mutate: updateBookmark, isPending: isDeletingSummary } = useUpdateBookmark({ onSuccess: () => { toast({ message: "Summary deleted!", showProgress: false, }); }, onError: () => { toast({ message: "Failed to delete summary", showProgress: false, }); }, }); // Only show for LINK bookmarks if (bookmark.content.type !== BookmarkTypes.LINK) { return null; } // If there's a summary, show it if (bookmark.summary) { return ( {!isExpanded && ( setIsExpanded(true)} className="rounded-md bg-gray-100 py-2 dark:bg-gray-800" > Show more )} {isExpanded && !readOnly && ( resummarize({ bookmarkId: bookmark.id })} disabled={isResummarizing} className="rounded-full bg-gray-200 p-2 dark:bg-gray-700" > {isResummarizing ? ( ) : ( )} updateBookmark({ bookmarkId: bookmark.id, summary: null }) } disabled={isDeletingSummary} className="rounded-full bg-gray-200 p-2 dark:bg-gray-700" > {isDeletingSummary ? ( ) : ( )} setIsExpanded(false)} className="rounded-full bg-gray-200 p-2 dark:bg-gray-700" > )} ); } // If no summary, show button to generate one if (readOnly) { return null; } return ( summarize({ bookmarkId: bookmark.id })} disabled={isSummarizing} className="rounded-lg bg-purple-500 p-3 dark:bg-purple-600" > {isSummarizing ? ( <> Generating summary... ) : ( <> Summarize with AI )} ); } const ViewBookmarkPage = () => { const insets = useSafeAreaInsets(); const { slug } = useLocalSearchParams(); const { toast } = useToast(); const { data: currentUser } = useWhoAmI(); if (typeof slug !== "string") { throw new Error("Unexpected param type"); } const { mutate: editBookmark, isPending: isEditPending } = useUpdateBookmark({ onSuccess: () => { toast({ message: "The bookmark has been updated!", showProgress: false, }); setEditedBookmark({}); }, }); const { mutate: deleteBookmark, isPending: isDeletionPending } = useDeleteBookmark({ onSuccess: () => { router.replace("dashboard"); toast({ message: "The bookmark has been deleted!", showProgress: false, }); }, }); const { data: bookmark, isPending, refetch, } = useAutoRefreshingBookmarkQuery({ bookmarkId: slug, }); // Check if the current user owns this bookmark const isOwner = currentUser?.id === bookmark?.userId; const [editedBookmark, setEditedBookmark] = React.useState<{ title?: string | null; note?: string; }>({}); if (isPending) { return ; } if (!bookmark) { return ( refetch()} /> ); } const handleDeleteBookmark = () => { Alert.alert( "Delete bookmark?", "Are you sure you want to delete this bookmark?", [ { text: "Cancel", style: "cancel" }, { text: "Delete", onPress: () => deleteBookmark({ bookmarkId: bookmark.id }), style: "destructive", }, ], ); }; const onDone = () => { const doDone = () => { if (router.canGoBack()) { router.back(); } else { router.replace("dashboard"); } }; if (Object.keys(editedBookmark).length === 0) { doDone(); return; } Alert.alert("You have unsaved changes", "Do you still want to leave?", [ { text: "Cancel", style: "cancel" }, { text: "Leave", onPress: doDone, }, ]); }; let title = null; switch (bookmark.content.type) { case BookmarkTypes.LINK: title = bookmark.title ?? bookmark.content.title; break; case BookmarkTypes.TEXT: title = bookmark.title; break; case BookmarkTypes.ASSET: title = bookmark.title ?? bookmark.content.fileName; break; } return ( ( Done ), }} /> setEditedBookmark((prev) => ({ ...prev, title })) } isPending={isEditPending} disabled={!isOwner} /> {isOwner && } setEditedBookmark((prev) => ({ ...prev, note: note ?? "" })) } isPending={isEditPending} disabled={!isOwner} /> {isOwner && ( )} Created {bookmark.createdAt.toLocaleString()} {bookmark.modifiedAt && bookmark.modifiedAt.getTime() !== bookmark.createdAt.getTime() && ( Modified {bookmark.modifiedAt.toLocaleString()} )} ); }; export default ViewBookmarkPage;